package cn.edu.dgut.css.sai.giteeoauth2demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequestEntityConverter;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.security.Principal;
import java.util.Collections;

/**
 * @author 黎志雄 老师 , lizhx@dgut.edu.cn , 东莞理工学院 网络空间安全学院 , https://css.dgut.edu.cn
 * Created by Sai on 2020/3/4.
 */
@SpringBootApplication
public class GiteeOauth2DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(GiteeOauth2DemoApplication.class, args);
    }

    @RestController
    static class UserController {
        /**
         * @see OAuth2AuthenticationToken
         */
        @RequestMapping("user")
        // spring mvc支持注入Authentication与Principal类型的参数
        Authentication SuccessLoginApi(Authentication authentication, Principal principal) {
            // 形参authentication 与 principal 是同一个对象，类型是 OAuth2AuthenticationToken
            // 设置断点观察，或查看下面的终端输出内容
            System.out.println("authentication = " + authentication + ", principal = " + principal);
            return authentication;
        }

        @RequestMapping("test1")
        // 若要@PreAuthorize注解生效，需@EnableGlobalMethodSecurity(prePostEnabled = true)
        // 用户默认会有ROLE_USER权限，可以用SpEl表达式hasRole('USER')判断权限，注：使用hasRole时ROLE_前缀可以不需要。
        // @PreAuthorize的优先级最高
        @PreAuthorize("hasRole('USER')")
        String test1() {
            return "访问/test1接口Success,你有USER权限";
        }

        @RequestMapping("test2")
        @PreAuthorize("hasAuthority('ROLE_USER')")
        String test2() {
            return "访问/test2接口Success,你有USER权限";
        }

        @RequestMapping("test3")
        @PreAuthorize("authenticated")
        String test3() {
            Authentication authentication = SecurityContextHolder.getContext()// 获取当前线程的Authentication对象，请求结束后会清除。
                    .getAuthentication();
            return "访问/test3接口Success<p>" + authentication;
        }

        @RequestMapping("test4")
        String test4(HttpServletRequest request) throws ServletException {
            // 使用servlet 3.0 API 退出登录
            request.logout();

            return "logout成功";
        }
    }

    @Configuration(proxyBeanMethods = false)
    @EnableGlobalMethodSecurity(prePostEnabled = true)// 加这个注解启用@PreAuthorize
    static class CustomSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .oauth2Login(this::oauth2LoginCustomizer)
                    .authorizeRequests().anyRequest().authenticated();// 所有请求都需要认证，authenticated()的意思是只要你认证成功即放行，不管你有什么权限。
        }

        void oauth2LoginCustomizer(OAuth2LoginConfigurer<HttpSecurity> configurer) {
            configurer
                    // 如果第二个参数设置为true，则登录成功后，强制重定向到指定的path；如果是false(默认值)，则登录成功后，则会重定向第一次被要求认证的页面。
                    .defaultSuccessUrl("/user", true)
                    .tokenEndpoint()
                        .accessTokenResponseClient(createGiteeTokenResponseClient())
                    .and()
                    .userInfoEndpoint()
                        .userService(createGiteeOAuth2UserService());
        }

        private DefaultAuthorizationCodeTokenResponseClient createGiteeTokenResponseClient() {
            DefaultAuthorizationCodeTokenResponseClient giteeTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
            giteeTokenResponseClient.setRequestEntityConverter(this::createGiteeOAuth2AccessTokenRequestEntityConverter);
            return giteeTokenResponseClient;
        }

        private DefaultOAuth2UserService createGiteeOAuth2UserService() {
            DefaultOAuth2UserService giteeOAuth2UserService = new DefaultOAuth2UserService();
            giteeOAuth2UserService.setRequestEntityConverter(this::createGiteeOAuth2UserServiceUserInfoRequestEntityConverter);
            return giteeOAuth2UserService;
        }

        /**
         * @see DefaultOAuth2UserService
         * @see OAuth2UserRequestEntityConverter
         */
        private RequestEntity<?> createGiteeOAuth2UserServiceUserInfoRequestEntityConverter(OAuth2UserRequest userRequest) {
            ClientRegistration clientRegistration = userRequest.getClientRegistration();
            URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())
                    .queryParam("access_token",userRequest.getAccessToken().getTokenValue())
                    .build()
                    .toUri();
            return RequestEntity.get(uri).headers(getDefaultTokenRequestHeaders()).build();// get请求
        }

        /**
         * @see DefaultAuthorizationCodeTokenResponseClient
         * @see OAuth2AuthorizationCodeGrantRequestEntityConverter
         */
        private RequestEntity<?> createGiteeOAuth2AccessTokenRequestEntityConverter(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
            ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
            MultiValueMap<String, String> queryParameters = buildQueryParameters(authorizationCodeGrantRequest);
            URI uri = UriComponentsBuilder
                        .fromUriString(clientRegistration.getProviderDetails().getTokenUri())
//                        .queryParams(queryParameters)
                        .build()
                        .toUri();
//            下面两句效果是一样。无论请求参数在url后面拼接，还是放在请求的body部分，码云Api都可以读取。
//            return RequestEntity.post(uri).headers(getDefaultTokenRequestHeaders()).build();
            return RequestEntity.post(uri).headers(getDefaultTokenRequestHeaders()).body(queryParameters);// post请求
        }

        private MultiValueMap<String, String> buildQueryParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
            ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
            OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange();
            MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
            // 都是必填的参数
            formParameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
            formParameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
            formParameters.add(OAuth2ParameterNames.REDIRECT_URI, authorizationExchange.getAuthorizationRequest().getRedirectUri());
            formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
            formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
            return formParameters;
        }

        /**
         * 码云的所有 openAPI 请求的header如果没有User-Agent会报错。
         * 码云官方issues: https://gitee.com/oschina/git-osc/issues/IDBSA
         */
        private HttpHeaders getDefaultTokenRequestHeaders() {
            HttpHeaders headers = new HttpHeaders();
            headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
            // 模拟构造一个User-Agent的header,发挥你的想象。
            headers.put("User-Agent", Collections.singletonList("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36"));
            return headers;
        }
    }
}
